package com.personalkeeper;


import java.util.NoSuchElementException;
import java.util.StringTokenizer;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract.PhoneLookup;
import android.telephony.SmsMessage;
import android.util.Log;
import static com.personalkeeper.PersonalKeeper.LATITUDE;
import static com.personalkeeper.PersonalKeeper.LONGITUDE;
import static com.personalkeeper.PersonalKeeper.NAME;
import static com.personalkeeper.PersonalKeeper.ADDRESS;
import static com.personalkeeper.PersonalKeeper.DATE;
import static com.personalkeeper.PersonalKeeper.ACTION_SMS_RECEIVED;
import static com.personalkeeper.PersonalKeeper.REQUEST;

public class SMSReceiver extends BroadcastReceiver {

    private static final String TAG          = SMSReceiver.class.toString();

    public static final String APP_NAME_HEADER = "@personalkeeper";
    public static final String LOCATION_HEADER = "@location";
    public static final String REQUEST_HEADER  = "@request";

    private static class ParsedSMS {

        private Double mLatitude, mLongitude;
        private boolean mRequest;

        public Double getLatitude() {
            return mLatitude;
        }

        public boolean isRequestAnswer() {
            return mRequest;
        }

        public Double getLongitude() {
            return mLongitude;
        }

        public void setLatitudeLongitude(Double latitude, Double longitude) {
            if ((latitude == null && longitude != null) ||
                    (latitude != null && longitude == null))
                throw new IllegalArgumentException(
                    "Longitude and latitude both must be null or both not null");
            this.mLatitude = latitude;
            this.mLongitude = longitude;
        }

        public ParsedSMS(Double latitude, Double longitude, boolean request) {
            setLatitudeLongitude(latitude, longitude);
            this.mRequest = request;
        }
    }

    private static enum State { NEXT_HEADER, NEXT_LATITUDE, NEXT_LONGITUDE, OK, FAIL };

    // if 'parse' returns 'null' and 'isMessageBelongToUs' returns 'false', that's common SMS
    // if 'parse' returns 'null' and 'isMessageBelongToUs' return 'true',
    //      message had incorrect format
    // if 'parse' returns not 'null', then 'isMessageBelongToUs' returns 'true'
    private static class MessageParser {

        private static final String TAG = MessageParser.class.toString();
        private boolean mMessageBelongToUs = false;

        public boolean isMessageBelongToUs() {
            return mMessageBelongToUs;
        }

        public ParsedSMS parse(String msg) {
            /* Message format:
             *      @personalkeeper
             *      @location
             *      latitude
             *      longitude
             *      @request
             */
            StringTokenizer tokenizer = new StringTokenizer(msg);

            State state = State.NEXT_HEADER;
            Double latitude = null;
            Double longitude = null;
            boolean request = false;
            int headersParsed = 0; // @location and @request headers parsed
            // if > 0, e.g. message contains information (good, 0 is bad)
            mMessageBelongToUs = false;
            while (state != State.OK && state != State.FAIL) {
                String token = null;
                try {
                    token = tokenizer.nextToken();
                } catch (NoSuchElementException e) {
                    state = (headersParsed == 0) ? State.FAIL : State.OK;
                }

                switch (state) {
                case NEXT_HEADER:
                    if (token.equals(APP_NAME_HEADER)) {
                        state = State.NEXT_HEADER;
                        mMessageBelongToUs = true;
                    } else if (token.equals(LOCATION_HEADER)) {
                        state = State.NEXT_LATITUDE;
                    } else if (token.equals(REQUEST_HEADER)) {
                        request = true;
                        headersParsed += 1;
                        state = State.NEXT_HEADER;
                    } else {
                        Log.d(TAG, "Bad header");
                        state = State.FAIL;
                    }
                    break;

                case NEXT_LATITUDE:
                    try {
                        latitude = Double.parseDouble(token);
                        state = State.NEXT_LONGITUDE;
                    } catch (NumberFormatException e) {
                        Log.d(TAG, "Bad latitude");
                        state = State.FAIL;
                    }
                    break;

                case NEXT_LONGITUDE:
                    try {
                        longitude = Double.parseDouble(token);
                        headersParsed += 1;
                        state = State.NEXT_HEADER;
                    } catch (NumberFormatException e) {
                        Log.d(TAG, "Bad longitude");
                        state = State.FAIL;
                    }
                    break;

                case OK:
                case FAIL:
                    break;
                }
            }

            if (state == State.OK)
                return new ParsedSMS(latitude, longitude, request);
            else
                return null;
        }
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        try {
            onReceiveHelper(context, intent);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    public void onReceiveHelper(Context context, Intent intent) {
        //---get the SMS message passed in---
        Bundle bundle = intent.getExtras();
        if (bundle != null) {
            //---retrieve the SMS message received---
            MessageParser parser = new MessageParser();
            Object[] pdus = (Object[]) bundle.get("pdus");
            for (Object pdu : pdus) {
                SmsMessage msg = SmsMessage.createFromPdu((byte[])pdu);
                String address = msg.getOriginatingAddress();
                String body = msg.getMessageBody();

                ParsedSMS message = parser.parse(body);
                if (parser.isMessageBelongToUs()) {
                    if (message != null) {
                        Intent serviceIntent = new Intent(context, GPSPollingService.class);
                        serviceIntent.setAction(ACTION_SMS_RECEIVED);
                        // TODO: add contact name
                        serviceIntent.putExtra(NAME, getNameFromPhone(context, address));
                        serviceIntent.putExtra(ADDRESS, address);
                        serviceIntent.putExtra(REQUEST, message.isRequestAnswer());
                        serviceIntent.putExtra(DATE, msg.getTimestampMillis());
                        if (message.getLatitude() != null && message.getLongitude() != null) {
                            serviceIntent.putExtra(LATITUDE, message.getLatitude());
                            serviceIntent.putExtra(LONGITUDE, message.getLongitude());
                        }
                        context.startService(serviceIntent);
                    } else
                        Log.d(TAG, "message has bad format: " + body);

                    abortBroadcast();
                }
            }
        }

        abortBroadcast();
    }

    private static String getNameFromPhone(Context context, String address) {
        String contact = null;
        Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(address));
        Cursor cs = context.getContentResolver().query(
                        uri,
                        new String[] {PhoneLookup.DISPLAY_NAME},
                        PhoneLookup.NUMBER + "='" + address + "'",
                        null,
                        null);
        if (cs != null) {
            try {
                if (cs.moveToFirst() && cs.getCount() > 0) {
                    contact = cs.getString(cs.getColumnIndex(PhoneLookup.DISPLAY_NAME));
                }
            } finally {
                cs.close();
            }
        }
        return contact;
    }
}
